from __future__ import annotations

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import os
import datetime
from typing import Optional, Dict, Any
import re
import sys

from .serial_client import SerialClient
from .ssh_client import SSHClient
from .session_store import SessionStore
from .local_shell import LocalShellClient


class TerminalView(ttk.Frame):
    def __init__(self, master: tk.Misc) -> None:
        super().__init__(master)
        # Toolbar inside terminal (line end + buttons)
        bar = ttk.Frame(self)
        bar.grid(row=0, column=0, columnspan=2, sticky="ew")
        ttk.Label(bar, text="Line end:").pack(side=tk.LEFT, padx=(0, 4))
        self.lineend_combo = ttk.Combobox(bar, values=["\\n", "\\r", "\\r\\n"], width=6)
        self.lineend_combo.set("\\n")
        self.lineend_combo.pack(side=tk.LEFT)
        ttk.Button(bar, text="Clear", command=self.clear).pack(side=tk.LEFT, padx=(6, 0))
        ttk.Button(bar, text="Save Log", command=self.save_log).pack(side=tk.LEFT, padx=(6, 0))
        # Timestamps: Date / Time / Milliseconds (per terminal)
        self.ts_date_var = tk.BooleanVar(value=False)
        self.ts_time_var = tk.BooleanVar(value=False)
        self.ts_ms_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(bar, text="日期(年-月-日)", variable=self.ts_date_var).pack(side=tk.LEFT, padx=(12, 0))
        ttk.Checkbutton(bar, text="时间(时:分:秒)", variable=self.ts_time_var).pack(side=tk.LEFT, padx=(6, 0))
        ttk.Checkbutton(bar, text="毫秒", variable=self.ts_ms_var).pack(side=tk.LEFT, padx=(6, 0))
        # ANSI 序列默认过滤（无 UI 开关）
        # （移除“压缩空行”按钮）

        self.text = tk.Text(self, wrap='word', height=24, width=100, font=("Consolas", 10))
        self.text.grid(row=1, column=0, sticky="nsew")
        self.scroll = ttk.Scrollbar(self, orient="vertical", command=self.text.yview)
        self.text.configure(yscrollcommand=self.scroll.set)
        self.scroll.grid(row=1, column=1, sticky="ns")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        self._buffer: list[str] = []
        self._sender = None
        # 关闭本地回显，避免与远端回显叠加产生空行
        self._echo_on_send = False
        self.input_start = None
        self._reset_input_mark()

        # Timestamp state
        self._at_line_start = True
        # 记录上次结尾是否为孤立的 '\r'（CRLF 被拆包时用）
        self._pending_cr = False

        self.text.bind('<KeyPress>', self._on_keypress, add=True)
        self.text.bind('<Button-1>', lambda e: self._ensure_cursor(e), add=True)
        # Paste handling: send clipboard text to remote, don't insert locally
        try:
            self.text.bind('<<Paste>>', self._on_paste, add=True)
        except Exception:
            pass

    def set_sender(self, sender) -> None:
        self._sender = sender

    def set_send_echo(self, enabled: bool) -> None:
        self._echo_on_send = bool(enabled)

    def set_show_time(self, enabled: bool) -> None:
        # Back-compat helper: when enabling, turn on Date+Time; when disabling, turn all off
        val = bool(enabled)
        try:
            if val:
                self.ts_date_var.set(True)
                self.ts_time_var.set(True)
            else:
                self.ts_date_var.set(False)
                self.ts_time_var.set(False)
                self.ts_ms_var.set(False)
        except Exception:
            pass

    def _ensure_cursor(self, event=None):
        try:
            if self.input_start and self.text.compare('insert', '<', self.input_start):
                self.text.mark_set('insert', self.input_start)
        except Exception:
            pass

    def _reset_input_mark(self) -> None:
        self.text.mark_set('insert', 'end-1c')
        self.input_start = self.text.index('end-1c')

    def _on_keypress(self, event: tk.Event):
        # True terminal-like behavior: send every key press to remote, no local echo
        ks = event.keysym
        # Enter sends configured line end
        if ks == 'Return':
            line_end = self.decode_lineend(self.lineend_combo.get())
            if self._sender:
                self._sender('', line_end)
            self.text.see('end')
            self._reset_input_mark()
            return 'break'
        # Backspace -> DEL (0x7F) which Linux shells commonly expect
        if ks == 'BackSpace':
            if self._sender:
                self._sender('\x7f', '')
            return 'break'
        # Tab triggers remote completion
        if ks == 'Tab':
            if self._sender:
                self._sender('\t', '')
            return 'break'
        # Arrow keys -> ANSI CSI sequences
        arrows = {
            'Up': '\x1b[A', 'Down': '\x1b[B', 'Right': '\x1b[C', 'Left': '\x1b[D',
            'Home': '\x1b[H', 'End': '\x1b[F', 'Insert': '\x1b[2~', 'Delete': '\x1b[3~',
            'Prior': '\x1b[5~', 'Next': '\x1b[6~',  # PageUp/PageDown
        }
        if ks in arrows:
            if self._sender:
                self._sender(arrows[ks], '')
            return 'break'
        # Ctrl combinations: Tk often provides control chars in event.char
        if event.char:
            # Send any character (printable or control) to remote
            if self._sender:
                self._sender(event.char, '')
            return 'break'
        # Block unknown keys from altering local buffer
        return 'break'

    def _on_paste(self, event: tk.Event):
        try:
            data = self.clipboard_get()  # type: ignore
        except Exception:
            data = ''
        if not data:
            return 'break'
        if self._sender:
            self._sender(data, '')
        return 'break'

    def append(self, text: str) -> None:
        if not text:
            return
        # 若上次以 '\r' 结尾，把它合并到本次头部，避免把 CR 与下个 LF 分离
        if getattr(self, "_pending_cr", False):
            text = '\r' + text
            self._pending_cr = False

        # 如果本次以孤立 '\r' 结尾，先暂存，等下次合并
        carry_cr = text.endswith('\r')
        if carry_cr:
            text_core = text[:-1]
        else:
            text_core = text

        # 归一化换行：CRLF -> LF；保留孤立 CR 以便在下方按行首覆盖处理
        text_core = text_core.replace('\r\n', '\n')
        # 始终过滤 ANSI 转义序列
        text_core = self._strip_ansi(text_core)

        ts_on = self._timestamp_enabled()
        # 渲染到 Text，支持退格、响铃(BEL)与回车覆盖
        i = 0
        while i < len(text_core):
            ch = text_core[i]
            if ch == '\a':  # BEL：忽略或可选蜂鸣
                try:
                    self.bell()
                except Exception:
                    pass
                i += 1
                continue
            if ch == '\b':  # 退格：删除当前行最后一个字符
                try:
                    line_start = self.text.index('end-1c linestart')
                    if self.text.compare('end-1c', '>', line_start):
                        self.text.delete('end-2c', 'end-1c')
                except Exception:
                    pass
                i += 1
                continue
            if ch == '\r':  # 回到行首（不换行）：删除当前行内容，准备覆盖
                try:
                    line_start = self.text.index('end-1c linestart')
                    self.text.delete(line_start, 'end-1c')
                except Exception:
                    pass
                # 不视为新行起始，避免重复打印时间戳
                self._at_line_start = False
                i += 1
                continue
            # 普通字符/换行：必要时加时间戳
            if self._at_line_start and ts_on:
                self.text.insert('end', self._timestamp_str())
            self.text.insert('end', ch)
            if ch == '\n':
                self._at_line_start = True
            else:
                self._at_line_start = False
            i += 1

        self.text.see('end')
        self._reset_input_mark()

        # 记录是否有挂起的 '\r'，与下次数据合并
        self._pending_cr = carry_cr

    # Patterns to strip common ANSI/VT escape sequences, including OSC/DCS blocks
    _ansi_csi_re = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")  # CSI sequences
    _ansi_osc_re = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")     # OSC ... BEL or ST
    _ansi_dcs_re = re.compile(r"\x1bP[^\x1b]*(?:\x1b\\)")                 # DCS ... ST
    _ansi_apc_re = re.compile(r"\x1b_[^\x1b]*(?:\x1b\\)")                 # APC ... ST
    _ansi_pm_re  = re.compile(r"\x1b\^[^\x1b]*(?:\x1b\\)")               # PM ... ST
    _ansi_sos_re = re.compile(r"\x1bX[^\x1b]*(?:\x1b\\)")                 # SOS ... ST

    def _strip_ansi(self, s: str) -> str:
        try:
            # Remove block-style sequences first (OSC/DCS/APC/PM/SOS)
            s = self._ansi_osc_re.sub('', s)
            s = self._ansi_dcs_re.sub('', s)
            s = self._ansi_apc_re.sub('', s)
            s = self._ansi_pm_re.sub('', s)
            s = self._ansi_sos_re.sub('', s)
            # Then remove standard CSI sequences
            s = self._ansi_csi_re.sub('', s)
            # Also remove simple single-char escapes like ESC7, ESC8, ESC( B, ESC)0, etc.
            s = re.sub(r"\x1b[()][0-9A-Za-z]", "", s)
            s = re.sub(r"\x1b[78]", "", s)
            return s
        except Exception:
            return s

    def _timestamp_enabled(self) -> bool:
        try:
            return bool(self.ts_date_var.get() or self.ts_time_var.get() or self.ts_ms_var.get())
        except Exception:
            return False

    def _timestamp_str(self) -> str:
        now = datetime.datetime.now()
        parts = []
        if self.ts_date_var.get():
            parts.append(f"{now:%Y-%m-%d}")
        if self.ts_time_var.get():
            parts.append(f"{now:%H:%M:%S}")
        stamp = " ".join(parts)
        if self.ts_ms_var.get():
            ms = f"{now.microsecond//1000:03d}"
            stamp = f"{stamp}.{ms}" if stamp else ms
        return f"[{stamp}] " if stamp else ""

    def clear(self) -> None:
        self.text.delete('1.0', 'end')
        self._buffer.clear()
        self._at_line_start = True
        self._reset_input_mark()

    def save_log(self) -> None:
        # 直接从文本获取内容，确保包含回车覆盖/退格后的最终可见文本
        content = self.text.get('1.0', 'end-1c')
        if not content:
            messagebox.showinfo("Save Log", "Nothing to save.")
            return
        path = filedialog.asksaveasfilename(title="Save Log", defaultextension=".txt", filetypes=[["Text Files", "*.txt"], ["All Files", "*.*"]])
        if not path:
            return
        try:
            with open(path, 'w', encoding='utf-8') as f:
                f.write(content)
            messagebox.showinfo("Save Log", f"Saved to {path}")
        except Exception as exc:
            messagebox.showerror("Save Log", f"Failed: {exc}")

    @staticmethod
    def decode_lineend(val: str) -> str:
        mapping = {"\\n": "\n", "\\r": "\r", "\\r\\n": "\r\n"}
        return mapping.get(val, "\n")


class SessionTab(ttk.Frame):
    def __init__(self, master: tk.Misc, title: str, client: Any) -> None:
        super().__init__(master)
        self.title = title
        self.client = client
        self.term = TerminalView(self)
        self.term.grid(row=0, column=0, sticky="nsew")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        # Wire send on Enter in terminal
        self.term.set_sender(self._send_line)

    def _send_line(self, text: str, line_end: str) -> None:
        # 允许空行，仅回车也会发送换行
        if not self.client or not getattr(self.client, "is_connected", False):
            messagebox.showwarning("Send", "Not connected.")
            return
        try:
            self.client.send_line(text, line_end=line_end)
        except Exception as exc:
            messagebox.showerror("Send", f"Send failed: {exc}")


def simple_input(master: tk.Misc, title: str, prompt: str, default: str = "") -> str:
    return simpledialog.askstring(title, prompt, initialvalue=default, parent=master) or ""


class App(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.title("Serial + SSH Tool")
        self.geometry("1100x700")

        # Layout: left sessions pane, right tabbed terminals
        self.panes = ttk.Panedwindow(self, orient=tk.HORIZONTAL)
        self.panes.pack(fill=tk.BOTH, expand=True)

        self.sidebar = ttk.Frame(self.panes, width=280)
        self.main = ttk.Frame(self.panes)
        self.panes.add(self.sidebar, weight=0)
        self.panes.add(self.main, weight=1)

        # Session store
        cfg_dir = os.path.join(os.path.expanduser("~"), ".serial_ssh_tool")
        self.store = SessionStore(os.path.join(cfg_dir, "sessions.json"))

        # Build UI sections
        self._build_sidebar()
        self._build_main()

        # Window close handler
        self.protocol("WM_DELETE_WINDOW", self.on_close)

        # For managing tabs -> session (map tab widget to client)
        self._tab_clients = {}

    # Sidebar with quick connect and sessions tree
    def _build_sidebar(self) -> None:
        # Quick connect (SSH)
        qc = ttk.LabelFrame(self.sidebar, text="Quick connect (SSH)")
        qc.pack(fill=tk.X, padx=8, pady=(8, 4))
        ttk.Label(qc, text="Host").grid(row=0, column=0, sticky="w")
        self.qc_host = ttk.Entry(qc, width=20)
        self.qc_host.grid(row=0, column=1, sticky="ew")
        ttk.Label(qc, text="Port").grid(row=0, column=2, sticky="w", padx=(6, 0))
        self.qc_port = ttk.Entry(qc, width=5)
        self.qc_port.insert(0, "22")
        self.qc_port.grid(row=0, column=3, sticky="w")

        ttk.Label(qc, text="User").grid(row=1, column=0, sticky="w", pady=(4, 0))
        self.qc_user = ttk.Entry(qc, width=20)
        self.qc_user.grid(row=1, column=1, sticky="ew", pady=(4, 0))
        # 明文密码输入
        ttk.Label(qc, text="Pass (plain)").grid(row=1, column=2, sticky="w", padx=(6, 0), pady=(4, 0))
        self.qc_pwd = ttk.Entry(qc, width=12)  # 移除 show='*'
        self.qc_pwd.grid(row=1, column=3, sticky="w", pady=(4, 0))

        connect_btn = ttk.Button(qc, text="Connect", command=self._qc_connect_ssh)
        connect_btn.grid(row=2, column=0, columnspan=4, sticky="ew", pady=(6, 0))
        qc.columnconfigure(1, weight=1)

        # Serial quick area
        sq = ttk.LabelFrame(self.sidebar, text="Serial")
        sq.pack(fill=tk.X, padx=8, pady=(4, 4))
        ttk.Label(sq, text="Port").grid(row=0, column=0, sticky="w")
        self.sq_port = ttk.Combobox(sq, values=SerialClient.list_available_ports(), width=18)
        self.sq_port.grid(row=0, column=1, sticky="w")
        ttk.Button(sq, text="Refresh", command=self._refresh_ports).grid(row=0, column=2, sticky="w", padx=(6, 0))
        ttk.Label(sq, text="Baud").grid(row=1, column=0, sticky="w", pady=(4, 0))
        self.sq_baud = ttk.Combobox(sq, values=["9600", "19200", "38400", "57600", "115200"], width=18)
        self.sq_baud.set("115200")
        self.sq_baud.grid(row=1, column=1, sticky="w", pady=(4, 0))
        ttk.Label(sq, text="Encoding").grid(row=2, column=0, sticky="w", pady=(4, 0))
        self.sq_enc = ttk.Combobox(sq, values=["utf-8", "gbk", "latin-1"], width=18)
        self.sq_enc.set("utf-8")
        self.sq_enc.grid(row=2, column=1, sticky="w", pady=(4, 0))
        ttk.Button(sq, text="Open", command=self._sq_open_serial).grid(row=2, column=2, sticky="w", padx=(6, 0), pady=(4, 0))

        # Local shell section
        ls = ttk.LabelFrame(self.sidebar, text="Local shell")
        ls.pack(fill=tk.X, padx=8, pady=(4, 4))
        ttk.Button(ls, text="Open PowerShell", command=self._open_local_shell).pack(fill=tk.X)

        # Sessions tree (Saved)
        tree_frame = ttk.LabelFrame(self.sidebar, text="Saved sessions")
        tree_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=(4, 8))
        self.tree = ttk.Treeview(tree_frame, show='tree')
        self.tree.pack(fill=tk.BOTH, expand=True)
        self._tree_root = self.tree.insert('', 'end', text='Saved', open=True)
        self._tree_ssh = self.tree.insert(self._tree_root, 'end', text='SSH', open=True)
        self._tree_serial = self.tree.insert(self._tree_root, 'end', text='Serial Ports', open=True)
        self._populate_saved_sessions()
        self._populate_serial_tree()
        self.tree.bind('<Double-1>', self._on_tree_double_click)

        # Saved session toolbar
        sb = ttk.Frame(self.sidebar)
        sb.pack(fill=tk.X, padx=8, pady=(0, 8))
        ttk.Button(sb, text="Save current", command=self._save_current_session).pack(side=tk.LEFT)
        ttk.Button(sb, text="Rename", command=self._rename_selected).pack(side=tk.LEFT, padx=(6, 0))
        ttk.Button(sb, text="Delete", command=self._delete_selected).pack(side=tk.LEFT, padx=(6, 0))

    def _build_main(self) -> None:
        # Top tabs for sessions
        self.notebook = ttk.Notebook(self.main)
        self.notebook.pack(fill=tk.BOTH, expand=True)
        # Tab close via middle click or context menu
        self.notebook.bind('<Button-2>', self._close_tab_event)
        self.notebook.bind('<Button-3>', self._tab_context_menu)
        self._tab_menu = tk.Menu(self, tearoff=0)
        self._tab_menu.add_command(label="Close", command=self._close_selected_tab)

    # Sidebar helpers
    def _refresh_ports(self) -> None:
        ports = SerialClient.list_available_ports()
        self.sq_port.configure(values=ports)
        self._populate_serial_tree()

    def _populate_serial_tree(self) -> None:
        # Clear existing children under serial
        for item in self.tree.get_children(self._tree_serial):
            self.tree.delete(item)
        for p in SerialClient.list_available_ports():
            self.tree.insert(self._tree_serial, 'end', text=p, values=(p,))

    def _populate_saved_sessions(self) -> None:
        # Clear SSH branch
        for item in self.tree.get_children(self._tree_ssh):
            self.tree.delete(item)
        for s in self.store.list():
            if s.get("type") == "ssh":
                text = s.get("name") or s["params"].get("host", "ssh")
                self.tree.insert(self._tree_ssh, 'end', text=text, values=(s["id"],), open=False)

    def _on_tree_double_click(self, event: tk.Event) -> None:
        item = self.tree.identify_row(event.y)
        if not item:
            return
        parent = self.tree.parent(item)
        text = self.tree.item(item, 'text')
        if parent == self._tree_serial:
            # Open serial with default baud
            self._open_serial_session(port=text, baud=115200)
        elif parent == self._tree_ssh:
            # Saved SSH session connect
            vals = self.tree.item(item, 'values')
            if vals:
                s = self.store.get(vals[0])
                if s and s.get("type") == "ssh":
                    p = s["params"]
                    self._open_ssh_session(host=p.get("host",""), port=int(p.get("port",22)), user=p.get("user",""), pwd=p.get("password",""))

    def _qc_connect_ssh(self) -> None:
        host = (self.qc_host.get() or '').strip()
        user = (self.qc_user.get() or '').strip()
        pwd = self.qc_pwd.get() or ''
        port = int(self.qc_port.get() or '22')
        if not host or not user:
            messagebox.showwarning("SSH", "Host and user are required.")
            return
        self._open_ssh_session(host=host, port=port, user=user, pwd=pwd)

    def _sq_open_serial(self) -> None:
        port = (self.sq_port.get() or '').strip()
        baud = int(self.sq_baud.get() or '115200')
        if not port:
            messagebox.showwarning("Serial", "Select a COM port.")
            return
        self._open_serial_session(port=port, baud=baud)

    def _save_current_session(self) -> None:
        # Save currently selected tab as a session (SSH only for now)
        sel = self.notebook.select()
        if not sel:
            messagebox.showinfo("Save", "No active session tab.")
            return
        # Find tab widget
        for child in self.notebook.winfo_children():
            if str(child) == sel:
                tab_widget = child
                break
        else:
            return
        client = self._tab_clients.get(tab_widget)
        if not client:
            return
        # 支持保存 SSH，并保存明文密码
        if isinstance(client, SSHClient):
            params = getattr(tab_widget, "ssh_params", None)
            if not params:
                # Fallback: 从标题解析（无密码）
                name = tab_widget.title
                host = ""; user = ""; port = 22
                try:
                    user_host, port_str = name.rsplit(":", 1)
                    user, host = user_host.split("@", 1)
                    port = int(port_str)
                except Exception:
                    pass
                params = {"host": host, "port": port, "user": user, "password": ""}
            default_name = tab_widget.title
            new_name = simple_input(self, "Save session", "Name:", default=default_name)
            if not new_name:
                return
            self.store.add(new_name, "ssh", params)  # 明文写入 sessions.json
            self._populate_saved_sessions()
        else:
            messagebox.showinfo("Save", "Saving serial sessions not yet supported.")

    def _rename_selected(self) -> None:
        sel = self.tree.selection()
        if not sel:
            return
        item = sel[0]
        parent = self.tree.parent(item)
        if parent != self._tree_ssh:
            return
        vals = self.tree.item(item, 'values')
        if not vals:
            return
        s = self.store.get(vals[0])
        if not s:
            return
        new_name = simple_input(self, "Rename", "New name:", default=s.get("name",""))
        if not new_name:
            return
        self.store.update(s["id"], name=new_name)
        self._populate_saved_sessions()

    def _delete_selected(self) -> None:
        sel = self.tree.selection()
        if not sel:
            return
        item = sel[0]
        parent = self.tree.parent(item)
        if parent != self._tree_ssh:
            return
        vals = self.tree.item(item, 'values')
        if not vals:
            return
        self.store.delete(vals[0])
        self._populate_saved_sessions()

    # Session creators
    def _open_serial_session(self, port: str, baud: int) -> None:
        enc = getattr(self, 'sq_enc', None).get() if hasattr(self, 'sq_enc') else 'utf-8'
        client = SerialClient(encoding=enc, on_data=None)
        tab = SessionTab(self.notebook, f"{port} @ {baud}", client)
        self.notebook.add(tab, text=f"{port}")
        self.notebook.select(tab)
        # Bind on_data to append to this tab
        client._on_data = lambda s, t=tab: t.term.after(0, lambda: t.term.append(s))
        try:
            client.connect(port=port, baudrate=baud)
            # （删除默认开启“压缩空行”的代码）
            tab.term.append(f"[Serial] Connected {port} @ {baud} ({enc})\n")
        except Exception as exc:
            messagebox.showerror("Serial", f"Connect failed: {exc}")
        # Track
        self._tab_clients[tab] = client

    def _open_ssh_session(self, host: str, port: int, user: str, pwd: str) -> None:
        client = SSHClient(on_data=None)
        title = f"{user}@{host}:{port}"
        tab = SessionTab(self.notebook, title, client)
        # 保存当前会话的连接参数（含明文密码），用于“保存会话”
        tab.ssh_params = {"host": host, "port": port, "user": user, "password": pwd}
        self.notebook.add(tab, text=host)
        self.notebook.select(tab)
        client._on_data = lambda s, t=tab: t.term.after(0, lambda: t.term.append(s))
        try:
            client.connect(host=host, port=port, username=user, password=pwd)
            tab.term.append(f"[SSH] Connected {title}\n")
        except Exception as exc:
            messagebox.showerror("SSH", f"Connect failed: {exc}")
        self._tab_clients[tab] = client

    # Append from worker threads safely to the tab
    def _append_from_thread(self, tab_id: str, data: str) -> None:
        # Kept for backward-compatibility; prefer binding lambda with tab reference.
        try:
            for child in self.notebook.winfo_children():
                if str(child) == tab_id:
                    child.term.after(0, lambda c=child, d=data: c.term.append(d))
                    return
        except Exception:
            pass

    # Tab close handlers
    def _close_tab_event(self, event: tk.Event) -> None:
        try:
            idx = self.notebook.index(f"@{event.x},{event.y}")
        except Exception:
            return
        self._close_tab_by_index(idx)

    def _tab_context_menu(self, event: tk.Event) -> None:
        try:
            idx = self.notebook.index(f"@{event.x},{event.y}")
            self.notebook.select(idx)
            self._tab_menu.post(event.x_root, event.y_root)
        except Exception:
            return

    def _close_selected_tab(self) -> None:
        try:
            idx = self.notebook.index(self.notebook.select())
        except Exception:
            return
        self._close_tab_by_index(idx)

    def _close_tab_by_index(self, idx: int) -> None:
        try:
            tab_widget_name = self.notebook.tabs()[idx]
        except Exception:
            return
        # Find the tab widget by name and close it
        for child in self.notebook.winfo_children():
            if str(child) == tab_widget_name:
                client = self._tab_clients.pop(child, None)
                if client:
                    try:
                        client.disconnect()
                    except Exception:
                        pass
                self.notebook.forget(child)
                child.destroy()
                break

    def on_close(self) -> None:
        # Disconnect all
        for client in list(self._tab_clients.values()):
            try:
                client.disconnect()
            except Exception:
                pass
        self._tab_clients.clear()
        self.destroy()

    # Local PowerShell session
    def _open_local_shell(self) -> None:
        client = LocalShellClient(on_data=None)
        tab = SessionTab(self.notebook, "Local PowerShell", client)
        self.notebook.add(tab, text="PowerShell")
        self.notebook.select(tab)
        # Bind on_data to append to this tab
        client._on_data = lambda s, t=tab: t.term.after(0, lambda: t.term.append(s))
        try:
            client.connect()
            tab.term.append("[Local] Shell started\n")
            # PowerShell expects CRLF and does remote echo; avoid local echo
            try:
                tab.term.lineend_combo.set("\\r\\n")
                tab.term.set_send_echo(False)
            except Exception:
                pass
        except Exception as exc:
            messagebox.showerror("Local", f"Start failed: {exc}")
        # Track
        self._tab_clients[tab] = client


def run() -> None:
    app = App()
    app.mainloop()

if __name__ == "__main__":
    run()
